// Package op defines the possible operations that can be emitted when
// compiling a Nix file, and interpreted by the runtime.
package op

type Op byte

const (
	// Invalid is a sentinel value to signal invalid bytecode.
	Invalid Op = iota

	// Constant pushes a constant onto the stack.
	Constant

	// Pop discards a value from the stack.
	Pop

	// Invert inverts the boolean at the top of the stack.
	Invert

	// Negate negates the number at the top of the stack.
	Negate

	// Add adds two values together for supported types.
	Add

	// Sub subtracts one number from another.
	Sub

	// Mul multiplies two numbers.
	Mul

	// Div divides one number by another.
	Div

	// Equal checks two Nix values for equality.
	Equal

	// List constructs a list. Takes an argument describing the
	// number of list elements, which are then taken from the top
	// of the stack.
	List

	// Concat concatenates two lists together.
	Concat

	// Update merges two attribute sets.
	Update

	// Interpolate concatenates a given number of string fragments.
	Interpolate

	// Attrs constructs an attribute sets. Takes an argument describing the
	// number of key/value *pairs* to add, which are then taken from the top
	// of the stack.
	Attrs

	// Select selects a value from an attribute set using the (string) key
	// at the top of the stack.
	Select

	// CloseScope removes locals from the stack after exiting a scope (e.g.
	// when returning the value of a `let`-expression). The final expression
	// value is left on the stack.
	CloseScope

	// GetLocal copies the value at the stack index provided by its argument
	// to the top of the stack. This is used for accessing local variables.
	GetLocal

	// GetCapture copies the value at the capture index provided by its index to
	// the top of the stack. This is used for accessing captures.
	GetCapture

	// Call calls the function at the top of the stack. Its argument is
	// taken from the next stack slot.
	Call

	// Jump in the bytecode by the specified number of bytes.
	Jump

	// JumpIfFalse is like jump, but jumps only if the value at the top of
	// the stack is false. Otherwise execution continues as normal.
	JumpIfFalse

	// JumpIfTrue is like jump, but jumps only if the value at the top of
	// the stack is true.
	JumpIfTrue

	// Capture instructs the runtime to capture a local in the target thunk or
	// closure. Captures take *two* arguments: The stack index of the thunk or
	// closure, and the position of the local to capture.
	Capture

	// Force evaluates the thunk at the top of the stack, or leaves it
	// untouched if it is already evaluated.
	Force

	// AssertBool ensures that the value at the top of the stack is a boolean,
	// and throws an error otherwise.
	AssertBool

	// Return is the final instruction in each chunk. It does not
	// have an effect, but can help simplify frame management
	// logic in the runtime.
	//
	// Note: This MUST always be the final opcode in the iota. Do
	// not move it!
	Return
)

// FromByte converts a raw byte into an operation and verifies its
// validity. For bytes that are not valid operations, `Invalid` is
// always returned.
func FromByte(b byte) Op {
	if b <= byte(Return) {
		return Op(b)
	}

	return Invalid
}

var opNames = map[Op]string{
	Invalid:     "Invalid",
	Constant:    "Constant",
	Pop:         "Pop",
	Invert:      "Invert",
	Negate:      "Negate",
	Add:         "Add",
	Sub:         "Sub",
	Mul:         "Mul",
	Div:         "Div",
	Equal:       "Equal",
	List:        "List",
	Concat:      "Concat",
	Update:      "Update",
	Interpolate: "Interpolate",
	Attrs:       "Attrs",
	Select:      "Select",
	CloseScope:  "CloseScope",
	GetLocal:    "GetLocal",
	GetCapture:  "GetCapture",
	Call:        "Call",
	Jump:        "Jump",
	JumpIfFalse: "JumpIfFalse",
	JumpIfTrue:  "JumpIfTrue",
	Capture:     "Capture",
	Force:       "Force",
	AssertBool:  "AssertBool",
	Return:      "Return",
}

func (o Op) String() string {
	return opNames[o]
}

type Arg byte

const (
	None Arg = iota
	Uvarint
	Fixed
	Multiple
)

var opsWithArg = map[Op]Arg{
	Constant:    Uvarint,
	List:        Uvarint,
	Attrs:       Uvarint,
	CloseScope:  Uvarint,
	GetLocal:    Uvarint,
	GetCapture:  Uvarint,
	Interpolate: Uvarint,
	Jump:        Fixed,
	JumpIfFalse: Fixed,
	JumpIfTrue:  Fixed,
	Capture:     Multiple,
}

func (o Op) HasArg() Arg {
	return opsWithArg[o]
}
